package com.hup.vd.m3u8;

import com.hup.utils.commons.exception.AppCommonException;
import com.hup.utils.commons.interfaces.Runners.Runner2;
import com.hup.vd.m3u8.FfmpegUtil.FfmpegException;
import com.hup.vd.util.RestTemplateUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.io.FilenameUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

/**
 * 简单的m3u8解析类
 *
 * @author hugan
 * @date 2021/12/14
 */
@Slf4j
public abstract class M3u8Util {
    public static final String M3U8_HEAD = "#EXTM3U";
    public static final String M3U8_TAIL = "#EXT-X-ENDLIST";
    public static final String URL_TAG = "#EXTINF:";
    public static final String M3U8_EXT = ".m3u8";

    /**
     * 根据url得到m3u8数据,
     * 支持简单的master类型/多流类型的数据解析
     *
     * @param restTemplate 必须配置好timeout
     */
    public static M3u8Data parseFromUrl(RestTemplate restTemplate, String urlText, Consumer<HttpHeaders> headersSetter) throws M3u8DataException {
        ResponseEntity<String> response = downloadM3u8File(restTemplate, urlText, headersSetter);
        String content = response.getBody();
        assert content != null;
        content = content.trim();//避免首尾的换行符造成异常
        //处理重定向问题
        String redirectUrl = RestTemplateUtil.getRedirectUrl(response);
        if (redirectUrl != null) {
            log.info("处理重定向={}", redirectUrl);
            urlText = redirectUrl;
        }
        if (!content.startsWith(M3U8_HEAD)) throw new M3u8DataException("文件头异常");
        //一般的media类型的文本
        if (!content.contains(M3U8_EXT))
            return parseMediaPlaylistData(content, urlText, restTemplate, headersSetter);

        /*
        解析master类型的文本,至少含一个m3u8子流的url
        只取第一个url
         */
        try {
            String[] lines = content.split("\n");
            for (String line : lines) {
                if (!line.contains(M3U8_EXT)) continue;
                String url = getUrl(urlText, line.trim());
                content = downloadM3u8File(restTemplate, url, headersSetter).getBody();
                return parseMediaPlaylistData(content, url, restTemplate, headersSetter);
            }
        } catch (MalformedURLException e) {
            throw new M3u8DataException("url解析异常:" + e.getMessage());
        }
        throw new M3u8DataException("不会出现的异常");
    }

    /**
     * url拼接
     *
     * @param host 完整的url,或仅host
     * @param uri 完整的url,或path片段: '/path/xxx', 'xx/path/xxx', './xxx', 'http://xxx'等
     */
    public static String getUrl(String host, String uri) throws MalformedURLException {
        URL url = new URL(new URL(host), uri);
        return url.toString();
    }

    /**
     * 请求m3u8文件,用于处理重定向后获取重定向地址
     *
     * @apiNote 能自动解析302重定向, 下载后面的url的m3u8内容
     */
    public static ResponseEntity<String> downloadM3u8File(RestTemplate restTemplate, String url, Consumer<HttpHeaders> headersSetter) {
        log.info("开始下载={}", url);
        HttpHeaders headers = new HttpHeaders();
        if (headersSetter != null) headersSetter.accept(headers);
        HttpEntity<?> requestEntity = new HttpEntity<>(headers);
        return restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class);
    }

    /**
     * 解析media类型的m3u8文件
     * https://blog.csdn.net/qq494257084/article/details/103550902
     * https://mvnrepository.com/search?q=m3u8
     * .关于playlist类型,有:master,media, https://www.jianshu.com/p/e97f6555a070
     * .关于hostUrl:某些接口会返回的m3u8文本中,url是只有path的
     *
     * @param m3u8Url m3u8Url的下载地址(重定向后的,若有); 只在[下载文件]调用时不为null,[解释本地文件]时为null
     * @throws M3u8DataException 内容异常或不支持格式
     */
    public static M3u8Data parseMediaPlaylistData(String content, String m3u8Url,
            RestTemplate restTemplate, Consumer<HttpHeaders> headersSetter) throws M3u8DataException {
        log.info("解析m3u8内容, m3u8Url={}", m3u8Url);
        content = content.trim();//避免首尾的换行符造成异常
        if (!content.startsWith(M3U8_HEAD)) throw new M3u8DataException("文件头异常");
        if (content.contains(M3U8_EXT)) throw new M3u8DataException("无url时,不能解析多流数据");
        if (!content.endsWith(M3U8_TAIL)) throw new M3u8DataException("文件尾异常");

        M3u8Data res = new M3u8Data();
        String[] lines = content.split("\n");
        //解析配置标签
        for (String line : lines) {
            for (KeyParser keyParser : KeyParser.values()) if (keyParser.parseKey(line, res)) break;
            if (line.startsWith(URL_TAG)) break;
        }
        M3u8DecryptUtil.parseDecryptKey(res, m3u8Url, restTemplate, headersSetter);

        float totalSpan = 0;
        ArrayList<String> urls = new ArrayList<>();

        int urlTagLength = URL_TAG.length();
        boolean hadCheckedUrl = false;
        boolean needHostUrl = false;
        try {
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i];
                if (!line.startsWith(URL_TAG) || i + 1 > lines.length - 1) {
                    //跳过前面n行,或无i+1行则跳过
                    continue;
                }
                //时长
                float span = Float.parseFloat(line.substring(urlTagLength, line.length() - 1));
                totalSpan += span;
                //url
                String url = lines[i + 1];
                if (!hadCheckedUrl) {
                    if (!url.startsWith("http")) {
                        log.info("需要补全hostUrl");
                        needHostUrl = true;
                    }
                    hadCheckedUrl = true;
                }
                if (needHostUrl) {
                    if (m3u8Url == null) throw new M3u8DataException("需要传入m3u8Url");
                    url = getUrl(m3u8Url, url);
                    lines[i + 1] = url;//重新赋值,用于下文 setSourceText
                }
                urls.add(url);
                i++;
                //log.info("url={}, {}", i, url);
            }
        } catch (MalformedURLException e) {
            throw new M3u8DataException("url解析异常:" + e.getMessage());
        }
        res.setSpan(totalSpan);
        if (urls.isEmpty()) throw new M3u8DataException("片段列表为空");
        res.setUrls(urls);

        if (needHostUrl) {
            //使用上文 重新赋值后的lines 拼接content
            StringBuilder sb = new StringBuilder(content.length() + urls.size() * m3u8Url.length());//长度只是近似值
            for (String line : lines) {
                sb.append(line).append('\n');
            }
            content = sb.toString();
        }
        res.setSourceText(content);
        return res;
    }

    /**
     * 使用ffmpeg合并ts文件
     *
     * @param fileNameList 文件名列表,不支持中文文件名
     */
    public static void mergeTs(File workingDirectory, List<String> fileNameList, File targetFile) throws FfmpegException {
        if (targetFile.exists()) {
            throw new AppCommonException("文件合并异常: 目标文件已存在? " + targetFile.getAbsolutePath());
        }

        //生成[文件列表] 用于ffmpeg
        File mergeList = new File(workingDirectory, "_mergeList.txt");
        try (BufferedWriter writerMergeList = Files.newBufferedWriter(
                Paths.get(mergeList.getAbsolutePath()), StandardCharsets.UTF_8);) {
            for (String file : fileNameList) {
                writerMergeList.write("file '");
                writerMergeList.write(file);
                writerMergeList.write("'");
                writerMergeList.newLine();
            }
        } catch (IOException e) {
            throw new AppCommonException("文件合并异常: 生成合并列表异常, " + e.getMessage());
        }

        /*
        调用ffmpeg:先输出temp文件, 成功后再rename
        ffmpeg -f concat -i _mergeList.txt -c copy output.mp4  //只保留一个[音频流]
        ffmpeg -f concat -i _mergeList.txt -map 0 -c:v copy -c:a copy -c:s copy _temp_merge.mp4 //保留原有[视频流,音频流,字幕流]
        [-safe]作用:安全检查, 1(缺省值)-启用检查,拦截特殊字符,路径攻击等,但也会拦截中文!!; 0-禁用检查
        ["默认音频顺序"不同]问题
            potplayer,(一视频)不同ts文件,会有默认音频"混乱",音频流菜单的流顺序 可能会随机不同(如有时英文在前,有时在后);
            实际上,只是菜单顺序不同,[音频id]是相同的, ffmpeg合并后,没出现'混乱'的情况
         */
        String ext = FilenameUtils.getExtension(targetFile.getName());
        File mergeResult = new File(workingDirectory, "_temp_merge." + ext);

        CommandLine cmd = FfmpegUtil.createCommandLine();
        String cmdText = "-f concat -i " + mergeList.getName() + " -map 0 -c:v copy -c:a copy -c:s copy " + mergeResult.getName();
        cmd.addArguments(cmdText.split(" "));//注意文件名不能有空格
        //log.info("cmd={}", cmd);

        FfmpegUtil.exec(cmd, workingDirectory, "文件合并异常: ");

        //没异常, 代表执行成功
        boolean rename = mergeResult.renameTo(targetFile);
        if (!rename) {
            throw new AppCommonException("文件移动失败? " + mergeResult.getAbsolutePath());
        }
    }

    public static class M3u8DataException extends Exception {
        public M3u8DataException(String message) {
            super("m3u8解析异常:" + message);
        }
    }

    /**
     * 配置标签定义与解析,
     * https://www.cnblogs.com/shakin/p/3870442.html
     */
    private enum KeyParser {
        VERSION("#EXT-X-VERSION:", M3u8Data::setVersion),
        /**
         * 代表数据需要解密
         */
        DECRYPT_KEY("#EXT-X-KEY:", M3u8Data::setDecryptMethod),
        TARGET_DURATION("#EXT-X-TARGETDURATION:", M3u8Data::setTargetDuration);

        /**
         * 带[#,:]的key内容
         */
        private final String key;
        /**
         * 实现根据字符串调用setter方法,另一种方法是反射
         */
        private final Runner2<M3u8Data, String> handler;

        KeyParser(String key, Runner2<M3u8Data, String> handler) {
            this.key = key;
            this.handler = handler;
        }

        /**
         * @return true-匹配到目标标签
         */
        public boolean parseKey(String line, M3u8Data data) {
            if (!line.startsWith(key)) return false;
            String value = line.substring(key.length());
            handler.run(data, value);
            return true;
        }
    }

}
